/********************************************************************************
 * Copyright (c) 2011-2017 Red Hat Inc. and/or its affiliates and others
 *
 * This program and the accompanying materials are made available under the 
 * terms of the Apache License, Version 2.0 which is available at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * SPDX-License-Identifier: Apache-2.0 
 ********************************************************************************/
package org.eclipse.ceylon.compiler.typechecker.analyzer;

import static org.eclipse.ceylon.compiler.typechecker.analyzer.AnalyzerUtil.getLastExecutableStatement;
import static org.eclipse.ceylon.compiler.typechecker.tree.TreeUtil.eliminateParensAndWidening;

import org.eclipse.ceylon.compiler.typechecker.tree.CustomTree;
import org.eclipse.ceylon.compiler.typechecker.tree.Node;
import org.eclipse.ceylon.compiler.typechecker.tree.Tree;
import org.eclipse.ceylon.compiler.typechecker.tree.Tree.MemberOrTypeExpression;
import org.eclipse.ceylon.compiler.typechecker.tree.Tree.Parameter;
import org.eclipse.ceylon.compiler.typechecker.tree.Tree.Super;
import org.eclipse.ceylon.compiler.typechecker.tree.Visitor;
import org.eclipse.ceylon.model.typechecker.model.Declaration;
import org.eclipse.ceylon.model.typechecker.model.FunctionOrValue;
import org.eclipse.ceylon.model.typechecker.model.Scope;
import org.eclipse.ceylon.model.typechecker.model.Type;
import org.eclipse.ceylon.model.typechecker.model.TypeDeclaration;
import org.eclipse.ceylon.model.typechecker.model.TypedDeclaration;
import org.eclipse.ceylon.model.typechecker.model.Value;
/**
 * Validates that the initializer of a class does not leak 
 * self-references to the instance being initialized.
 * 
 * @author Gavin King
 *
 */
public class SelfReferenceVisitor extends Visitor {
    
    private final TypeDeclaration typeDeclaration;
    private Tree.Statement lastExecutableStatement;
    private boolean declarationSection = false;
    private int nestedLevel = -1;
    private boolean defaultArgument;
    private boolean inCaseTypesList;
    private boolean inExtends;

    public SelfReferenceVisitor(TypeDeclaration td) {
        typeDeclaration = td;
    }
    
    private Declaration resolveTypeAliases(Declaration member) {
        if (member instanceof TypeDeclaration) {
            TypeDeclaration superclass = 
                    (TypeDeclaration) member;
            while (superclass!=null && 
                    superclass.isAlias()) {
                Type et = superclass.getExtendedType();
                superclass = 
                        et == null ? null : 
                            et.getDeclaration();
            }
            member = superclass;
        }
        return member;
    }

    private void visitExtendedType(
            Tree.ExtendedTypeExpression that) {
        if (that.hasErrors()) return;
        Scope scope = that.getScope();
        //VERY UGLY!!
        //Note: super has a special meaning in any 
        //      extends clause where it refers to the 
        //      supertype of the outer class
        CustomTree.ExtendedTypeExpression ete =
                (CustomTree.ExtendedTypeExpression) that;
        Tree.SimpleType type = ete.getType();
        if (type instanceof Tree.QualifiedType) {
            Tree.QualifiedType qt = 
                    (Tree.QualifiedType) type;
            if (qt.getOuterType() instanceof Tree.SuperType) {
                scope = scope.getContainer();
            }
        }
        
        Declaration member = 
                resolveTypeAliases(that.getDeclaration());
        if (member!=null 
                && !member.isStatic()
                && isInherited(scope, member)) {
            Declaration container = 
                    (Declaration) 
                        member.getContainer();
            that.addError("inherited member class may not be extended in initializer of '" +
                    typeDeclaration.getName() + 
                    "': '" + member.getName() + 
                    "' is inherited by '" + 
                    typeDeclaration.getName() +
                    "' from '" + 
                    container.getName() + "'");
        }
    }

    private void checkReference(
            Tree.MemberOrTypeExpression that) {
        if (that.hasErrors()) return;
        Declaration member = 
                resolveTypeAliases(that.getDeclaration());
        if (member!=null) {
            if (isInheritingValueConstructor(that, member)) {
                Declaration container = 
                        (Declaration) 
                        member.getContainer();
                if (container.equals(typeDeclaration)) {
                    that.addError("value constructor '" +
                            member.getName() +
                            "' may not be used in initializer of class '" +
                            typeDeclaration.getName() + 
                            "': '" + member.getName() +
                            "' is a value constructor of '" +
                            container.getName() + "'");
                }
                else {
                    that.addError("value constructor '" +
                            member.getName() +
                            "' may not be used in initializer of superclass '" +
                            typeDeclaration.getName() + 
                            "': '" + member.getName() + 
                            "' is a value constructor of '" +
                            container.getName() + "', which inherits '" +
                            typeDeclaration.getName() + "'");
                }
            }
            else if (isInheritingAnonymousClass(member)) {
                if (member.equals(typeDeclaration)) {
                    that.addError("anonymous class '" +
                            member.getName() +
                            "' may not be used in its own initializer: '" +
                            member.getName() +
                            "' is an anonymous class");
                }
                else {
                    that.addError("anonymous class '" +
                            member.getName() +
                            "' may not be used in initializer of superclass '" +
                            typeDeclaration.getName() + 
                            "': '" + member.getName() +
                            "' is an anonymous class that inherits '" +
                            typeDeclaration.getName() + "'");
                }
            }
        }
    }

    private void checkMemberReference(
            Tree.MemberOrTypeExpression that) {
        if (that.hasErrors()) return;
        Declaration member = 
                resolveTypeAliases(that.getDeclaration());
        if (member!=null
                && !member.isStatic()
                && isInherited(that.getScope(), member)
                && !member.isJava()) {
            Declaration container = 
                    (Declaration) 
                        member.getContainer();
            if (inExtends) { //TODO: this condition is not quite right 
                             //      consider access to class member 
                             //      inside extends clause of inner class
                that.addError("inherited member may not be used in extends clause of '" +
                        typeDeclaration.getName() + 
                        "': '" + member.getName() + 
                        "' is inherited by '" + 
                        typeDeclaration.getName() +
                        "' from '" + 
                        container.getName() + "'");
            }
            else {
                that.addError("inherited member may not be used in initializer of '" +
                        typeDeclaration.getName() + 
                        "': '" + member.getName() + 
                        "' is inherited by '" + 
                        typeDeclaration.getName() +
                        "' from '" + 
                        container.getName() + "'");
            }
        }
    }
    
    /**
     * Is this a reference to a value constructor of the
     * type declaration, or of a class that inherits the
     * type declaration, from within the initializer section
     * of the type declaration?
     */
    private boolean isInheritingValueConstructor(Tree.Primary that, 
            Declaration member) {
        if (member instanceof Value) {
            return inInitializer() &&
                    !inCaseTypesList &&
                    member.isConstructor() && 
                    ((TypeDeclaration) member.getContainer())
                        .inherits(typeDeclaration);
        }
        else {
            return false;
        }
    }

    private boolean isInheritingAnonymousClass(Declaration member) {
        if (member instanceof Value) {
            Value value = (Value) member;
            if (inInitializer() && !inCaseTypesList) {
                TypeDeclaration vtd = 
                        value.getTypeDeclaration();
                return vtd!=null &&
                        vtd.isObjectClass() && 
                        vtd.inherits(typeDeclaration);
            }
            else { 
                return false;
            }
        }
        else {
            return false;
        }
    }
    
    /**
     * Is this a reference to a member inherited by the
     * type declaration from within the initializer section
     * of the type declaration?
     */
    private boolean isInherited(Scope scope, 
            Declaration member) {
        return inInitializer() &&
                scope.getInheritingDeclaration(member)
                    == typeDeclaration;
    }

    @Override
    public void visit(Tree.CaseTypes that) {
        inCaseTypesList = true;
        super.visit(that);
        inCaseTypesList = false;
    }
    
    @Override
    public void visit(Tree.AnnotationList that) {}

    @Override
    public void visit(Tree.ExtendedType that) {
        inExtends = true;
        super.visit(that);
        inExtends = false;
    }
    
    @Override
    public void visit(Tree.ExtendedTypeExpression that) {
        super.visit(that);
        visitExtendedType(that);
    }

    @Override
    public void visit(Tree.BaseMemberExpression that) {
        super.visit(that);
        checkMemberReference(that);
        checkReference(that);
    }

    @Override
    public void visit(Tree.BaseTypeExpression that) {
        super.visit(that);
        checkMemberReference(that);
        checkReference(that);
    }

    @Override
    public void visit(Tree.QualifiedMemberExpression that) {
        super.visit(that);
        if (isSelfReference(that.getPrimary())) {
            checkMemberReference(that);
        }
        checkReference(that);
    }

    @Override
    public void visit(Tree.QualifiedTypeExpression that) {
        super.visit(that);
        if (isSelfReference(that.getPrimary())) {
            checkMemberReference(that);
        }
        checkReference(that);
    }

    private boolean isSelfReference(Tree.Term that) {
        Tree.Term term = eliminateParensAndWidening(that);
        return directlyInBody() 
                && (term instanceof Tree.This 
                    || term instanceof Tree.Super) 
            || directlyInNestedBody() 
                && that instanceof Tree.Outer;
    }

    @Override
    public void visit(Tree.IsCondition that) {
        super.visit(that);
        if (inBody()) {
            Tree.Variable v = that.getVariable();
            if (v!=null) {
                Tree.SpecifierExpression se = 
                        v.getSpecifierExpression();
                if (se!=null) {
                    Tree.Term term = 
                            se.getExpression().getTerm();
                    if (directlyInBody() 
                            && term instanceof Tree.Super) {
                        term.addError("narrows 'super': '" + 
                                typeDeclaration.getName() + "'");
                    }
                    else if (mayNotLeakThis() 
                            && term instanceof Tree.This) {
                        term.addError("narrows 'this' in initializer: '" + 
                                typeDeclaration.getName() + "'");
                    }
                    else if (mayNotLeakOuter() 
                            && term instanceof Tree.Outer) {
                        term.addError("narrows 'outer' in initializer: '" + 
                                typeDeclaration.getName() + "'");
                    }
                }
            }
        }
    }
    
    @Override
    public void visit(Tree.ObjectDefinition that) {
        if (that.getAnonymousClass()==typeDeclaration) {
            nestedLevel=0;
            super.visit(that);
            nestedLevel=-1;
        }
        else if (inBody()){
            nestedLevel++;
            super.visit(that);
            nestedLevel--;
        }
        else {
            super.visit(that);
        }
    }
    
    @Override
    public void visit(Tree.ObjectArgument that) {
        if (that.getAnonymousClass()==typeDeclaration) {
            nestedLevel=0;
            super.visit(that);
            nestedLevel=-1;
        }
        else if (inBody()){
            nestedLevel++;
            super.visit(that);
            nestedLevel--;
        }
        else {
            super.visit(that);
        }
    }
    
    @Override
    public void visit(Tree.ObjectExpression that) {
        if (that.getAnonymousClass()==typeDeclaration) {
            nestedLevel=0;
            super.visit(that);
            nestedLevel=-1;
        }
        else if (inBody()){
            nestedLevel++;
            super.visit(that);
            nestedLevel--;
        }
        else {
            super.visit(that);
        }
    }
    
    @Override
    public void visit(Tree.TypeDeclaration that) {
        if (that.getDeclarationModel()==typeDeclaration) {
            nestedLevel=0;
            declarationSection = false;
            super.visit(that);
            nestedLevel=-1;
        }
        else if (inBody()){
            nestedLevel++;
            super.visit(that);
            nestedLevel--;
        }
        else {
            super.visit(that);
        }
    }
    
    @Override
    public void visit(Tree.InterfaceBody that) {
        if (directlyInBody()) {
            declarationSection = true;
            lastExecutableStatement = null;
            super.visit(that);
            declarationSection = false;
        }
        else {
            super.visit(that);
        }
    }

    private boolean directlyInBody() {
        return nestedLevel==0;
    }
    
    @Override
    public void visit(Tree.ClassBody that) {
        if (directlyInBody()) {
            Tree.Statement les = 
                    getLastExecutableStatement(that);
            declarationSection = les==null;
            lastExecutableStatement = les;
            super.visit(that);
            lastExecutableStatement = null;
            declarationSection = false;
        }
        else {
            super.visit(that);
        }
    }

    boolean mayNotLeakThis() {
        return !declarationSection && directlyInBody();
    }
    
    boolean mayNotLeakOuter() {
        return !declarationSection && directlyInNestedBody();
    }

    private boolean directlyInNestedBody() {
        return nestedLevel==1;
    }
    
    private boolean inBody() {
        return nestedLevel>=0;
    }
    
    private boolean inInitializer() {
        return inBody() && !declarationSection;
    }
    
    @Override
    public void visit(Tree.Statement that) {
        super.visit(that);
        if (inBody()) {
            declarationSection = declarationSection || 
                    that==lastExecutableStatement;
        }
    }
    
    private void checkSelfReference(Node that, Tree.Term term) {
        Tree.Term t = eliminateParensAndWidening(term);
        if (directlyInBody() && t instanceof Tree.Super) {
            that.addError("leaks 'super' reference: '" + 
                    typeDeclaration.getName() + "'");
        }    
        if (mayNotLeakThis() && t instanceof Tree.This) {
            that.addError("leaks 'this' reference in initializer: '" + 
                    typeDeclaration.getName() + "'");
        }    
        if (mayNotLeakOuter() && t instanceof Tree.Outer) {
            that.addError("leaks 'outer' reference in initializer: '" + 
                    typeDeclaration.getName() + "'");
        }
        if (typeDeclaration.isObjectClass() && 
                mayNotLeakAnonymousClass() && 
                t instanceof Tree.BaseMemberExpression) {
            Tree.BaseMemberExpression bme = 
                    (Tree.BaseMemberExpression) t;
            Declaration declaration = bme.getDeclaration();
            if (declaration instanceof TypedDeclaration) {
                TypedDeclaration td = 
                        (TypedDeclaration) declaration;
                if (td.getTypeDeclaration()==typeDeclaration) {
                    that.addError("anonymous class leaks self reference in initializer: '" + 
                            typeDeclaration.getName() + "'");
                }
            }
        }
        if (typeDeclaration.isObjectClass() && 
                mayNotLeakAnonymousClass() && 
                t instanceof Tree.QualifiedMemberExpression) {
            Tree.QualifiedMemberExpression qme = 
                    (Tree.QualifiedMemberExpression) t;
            if (qme.getPrimary() instanceof Tree.Outer) {
                Declaration declaration = qme.getDeclaration();
                if (declaration instanceof TypedDeclaration) {
                    TypedDeclaration td = 
                            (TypedDeclaration) declaration;
                    if (td.getTypeDeclaration()==typeDeclaration) {
                        that.addError("anonymous class leaks self reference in initializer: '" + 
                                typeDeclaration.getName() + "'");
                    }
                }
            }
        }
    }
    
    boolean mayNotLeakAnonymousClass() {
        return !declarationSection && inBody();
    }
    
    @Override
    public void visit(Parameter that) {
        boolean oda = defaultArgument;
        defaultArgument = true;
        super.visit(that);
        defaultArgument = oda;
    }
    
    @Override
    public void visit(Super that) {
        super.visit(that);
        if (defaultArgument) {
            that.addError("reference to super from default argument expression");
        }
    }

    @Override
    public void visit(Tree.Return that) {
        super.visit(that);
        Tree.Expression e = that.getExpression();
        if ( e!=null && inBody() ) {
            checkSelfReference(that, e.getTerm());    
        }
    }

    @Override
    public void visit(Tree.Throw that) {
        super.visit(that);
        Tree.Expression e = that.getExpression();
        if ( e!=null && inBody() ) {
            checkSelfReference(that, e.getTerm());    
        }
    }

    @Override
    public void visit(Tree.FunctionArgument that) {
        super.visit(that);
        Tree.Expression e = that.getExpression();
        if ( e!=null && inBody() ) {
            checkSelfReference(that, e.getTerm());    
        }
    }
    
    @Override
    public void visit(Tree.SpecifierOrInitializerExpression that) {
        super.visit(that);
        Tree.Expression e = that.getExpression();
        if ( e!=null && inBody() ) {
            checkSelfReference(that, e.getTerm());    
        }
    }
    
    @Override
    public void visit(Tree.SpecifierStatement that) {
        if ( inBody() ) {
            Tree.Term lt = that.getBaseMemberExpression();
            Tree.SpecifierExpression se = that.getSpecifierExpression();
            if (lt instanceof Tree.MemberOrTypeExpression && se!=null) {
                Tree.Expression e = se.getExpression();
                if (e!=null) {
                    if (e.getTerm() instanceof Tree.This) {
                        Tree.MemberOrTypeExpression mte = 
                                (Tree.MemberOrTypeExpression) lt;
                        Declaration d = mte.getDeclaration();
                        if (d instanceof FunctionOrValue) {
                            FunctionOrValue fov = 
                                    (FunctionOrValue) d;
                            if (fov.isLate()) {
                                lt.visit(this);
                                return; //NOTE: EARLY EXIT!!
                            }
                        }
                    }
                }
            }
        }
        super.visit(that);
    }

    @Override
    public void visit(Tree.AssignmentOp that) {
        super.visit(that);
        if ( inBody() ) {
            Tree.Term lt = that.getLeftTerm();
            Tree.Term rt = that.getRightTerm();
            if (lt instanceof Tree.MemberOrTypeExpression &&
                    rt instanceof Tree.This) {
                MemberOrTypeExpression mte = 
                        (Tree.MemberOrTypeExpression) lt;
                Declaration d = mte.getDeclaration();
                if (d instanceof FunctionOrValue) {
                    FunctionOrValue fov = 
                            (FunctionOrValue) d;
                    if (fov.isLate()) {
                        return; //NOTE: EARLY EXIT!!
                    }
                }
            }
            checkSelfReference(that, rt);    
        }
    }

    @Override
    public void visit(Tree.BinaryOperatorExpression that) {
        super.visit(that);
        if ( inBody() && !(that instanceof Tree.AssignmentOp) ) {
            checkSelfReference(that, that.getLeftTerm());
            checkSelfReference(that, that.getRightTerm());
        }
    }

    @Override
    public void visit(Tree.UnaryOperatorExpression that) {
        super.visit(that);
        if ( inBody() && !(that instanceof Tree.OfOp) ) {
            checkSelfReference(that, that.getTerm());
        }
    }
    
    @Override
    public void visit(Tree.IndexExpression that) {
        super.visit(that);
        if ( inBody() ) {
            checkSelfReference(that, that.getPrimary());
        }
        
    }

    @Override
    public void visit(Tree.Element that) {
        super.visit(that);
        if ( inBody() ) {
            checkSelfReference(that, that.getExpression());
        }
        
    }

    @Override
    public void visit(Tree.ElementRange that) {
        super.visit(that);
        if ( inBody() ) {
            checkSelfReference(that, that.getLowerBound());
            checkSelfReference(that, that.getUpperBound());
            checkSelfReference(that, that.getLength());
        }
        
    }

    @Override
    public void visit(Tree.WithinOp that) {
        super.visit(that);
        if ( inBody() ) {
            checkSelfReference(that, that.getTerm());
            checkSelfReference(that, that.getLowerBound());
            checkSelfReference(that, that.getUpperBound());
        }
    }

    @Override
    public void visit(Tree.ExpressionComprehensionClause that) {
        super.visit(that);
        if ( inBody() ) {
            Tree.Expression e = that.getExpression();
            if (e!=null) {
                checkSelfReference(that, e.getTerm());
            }
        }
    }

    @Override
    public void visit(Tree.ListedArgument that) {
        super.visit(that);
        if ( inBody() ) {
            Tree.Expression e = that.getExpression();
            if (e!=null) {
                checkSelfReference(that, e.getTerm());
            }
        }
    }

    @Override
    public void visit(Tree.SpreadArgument that) {
        super.visit(that);
        if ( inBody() ) {
            Tree.Expression e = that.getExpression();
            if (e!=null) {
                checkSelfReference(that, e.getTerm());
            }
        }
    }
    
    @Override
    public void visit(Tree.StringTemplate that) {
        super.visit(that);
        if ( inBody() ) {
            for (Tree.Expression e: that.getExpressions()) {
                if (e!=null) {
                    checkSelfReference(e, e.getTerm());
                }
            }
        }
    }

}
